"use client"; import { useEffect, useRef, useCallback, useState } from "react"; import { useSession } from "next-auth/react"; import { useParams, redirect, useRouter } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Loader2, Video, AlertTriangle, FileText, Clock, XCircle } from "lucide-react"; import { ConsultationNotes } from "@/components/appointments/ConsultationNotes"; import RecordsModal from "@/components/records/RecordsModal"; import type { Record as MedicalRecord } from "@/components/records/types"; import type { Appointment } from "@/types/appointments"; import { canJoinMeeting, getAppointmentTimeStatus } from "@/utils/appointments"; interface JitsiMeetExternalAPI { dispose: () => void; addEventListener: (event: string, handler: () => void) => void; } declare global { interface Window { JitsiMeetExternalAPI: new (domain: string, options: Record) => JitsiMeetExternalAPI; } } export default function MeetPage() { const router = useRouter(); const { data: session, status } = useSession(); const params = useParams(); const jitsiContainer = useRef(null); const jitsiApi = useRef(null); const isInitialized = useRef(false); const isLeavingIntentionally = useRef(false); const [showExitDialog, setShowExitDialog] = useState(false); const [showRecordsModal, setShowRecordsModal] = useState(false); const [appointment, setAppointment] = useState(null); const [loading, setLoading] = useState(true); const [accessDenied, setAccessDenied] = useState(false); const [denialReason, setDenialReason] = useState(""); const [jitsiToken, setJitsiToken] = useState(null); const [jitsiDomain, setJitsiDomain] = useState(""); const [jitsiRoomName, setJitsiRoomName] = useState(""); const [useJWT, setUseJWT] = useState(false); // Cargar información del appointment y JWT token useEffect(() => { const loadAppointment = async () => { try { const response = await fetch(`/api/appointments/${params.id}`); if (response.ok) { const data = await response.json(); setAppointment(data); // Validar acceso por tiempo const timeCheck = canJoinMeeting(data.fechaSolicitada); if (!timeCheck.canJoin) { setAccessDenied(true); setDenialReason(timeCheck.reason || "No puedes acceder a esta videollamada"); setLoading(false); return; } // Validar que la cita esté aprobada if (data.estado !== "APROBADA" && data.estado !== "COMPLETADA") { setAccessDenied(true); setDenialReason("Esta cita no está aprobada"); setLoading(false); return; } // Obtener JWT token para Jitsi const tokenResponse = await fetch(`/api/appointments/${params.id}/jitsi-token`); if (tokenResponse.ok) { const tokenData = await tokenResponse.json(); setJitsiToken(tokenData.token || null); setJitsiDomain(tokenData.domain || ""); setJitsiRoomName(tokenData.roomName || `appointment-${params.id}`); setUseJWT(tokenData.useJWT || false); } else { console.warn("No se pudo obtener JWT token, usando configuración por defecto"); setJitsiRoomName(`appointment-${params.id}`); // Si falla, intentar obtener el dominio del response aunque sea error const errorData = await tokenResponse.json().catch(() => ({})); if (errorData.domain) { setJitsiDomain(errorData.domain); } } } else { setAccessDenied(true); setDenialReason("No se pudo cargar la información de la cita"); } } catch (error) { console.error("Error loading appointment:", error); setAccessDenied(true); setDenialReason("Error al cargar la cita"); } finally { setLoading(false); } }; if (params.id) { loadAppointment(); } }, [params.id]); const handleCopyContent = (content: string) => { navigator.clipboard.writeText(content); // TODO: Add notification }; const handleDownloadReport = (record: MedicalRecord) => { const blob = new Blob([record.content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `reporte-medico-${record.id.slice(-8)}-${new Date().toISOString().split("T")[0]}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // TODO: Add notification }; const handleGeneratePDF = async (_record: MedicalRecord) => { // TODO: Implement PDF generation }; const initJitsi = useCallback(() => { if (!jitsiContainer.current || !session || isInitialized.current || !jitsiRoomName) return; isInitialized.current = true; const options: Record = { roomName: jitsiRoomName, width: "100%", height: 600, parentNode: jitsiContainer.current, configOverwrite: { startWithAudioMuted: false, startWithVideoMuted: false, prejoinPageEnabled: false, }, interfaceConfigOverwrite: { TOOLBAR_BUTTONS: [ "microphone", "camera", "closedcaptions", "desktop", "fullscreen", "fodeviceselection", "hangup", "chat", "settings", "videoquality", "filmstrip", "tileview", ], SHOW_JITSI_WATERMARK: false, SHOW_WATERMARK_FOR_GUESTS: false, }, userInfo: { displayName: `${session.user?.name || "Usuario"} ${session.user?.lastname || ""}`.trim(), email: session.user?.email || undefined, }, }; // Si se usa JWT, agregar el token if (useJWT && jitsiToken) { options.jwt = jitsiToken; } jitsiApi.current = new window.JitsiMeetExternalAPI(jitsiDomain, options); // Event listeners - Solo redirigir si el usuario salió desde Jitsi directamente jitsiApi.current.addEventListener("videoConferenceLeft", () => { // Dar un pequeño delay para que el beforeunload se procese setTimeout(() => { if (isLeavingIntentionally.current) { router.push("/appointments"); } }, 100); }); jitsiApi.current.addEventListener("readyToClose", () => { setTimeout(() => { if (isLeavingIntentionally.current) { router.push("/appointments"); } }, 100); }); }, [session, jitsiRoomName, jitsiDomain, jitsiToken, useJWT, router]); const handleExitClick = () => { setShowExitDialog(true); }; const handleConfirmExit = () => { isLeavingIntentionally.current = true; if (jitsiApi.current) { jitsiApi.current.dispose(); jitsiApi.current = null; } isInitialized.current = false; router.push("/appointments"); }; const handleCancelExit = () => { setShowExitDialog(false); }; // Interceptar cierre de pestaña o navegación useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { // Solo mostrar advertencia si NO es una salida intencional if (!isLeavingIntentionally.current) { e.preventDefault(); e.returnValue = "¿Estás seguro de que quieres salir de la videollamada?"; return e.returnValue; } }; window.addEventListener("beforeunload", handleBeforeUnload); return () => { window.removeEventListener("beforeunload", handleBeforeUnload); }; }, []); useEffect(() => { if (status === "loading" || !session || !jitsiContainer.current || isInitialized.current || !jitsiDomain || !jitsiRoomName) return; // Construir la URL del script usando el dominio configurado const scriptSrc = `https://${jitsiDomain}/external_api.js`; // Verificar si el script ya está cargado const existingScript = document.querySelector(`script[src="${scriptSrc}"]`); if (existingScript) { // Si el script ya existe y window.JitsiMeetExternalAPI está disponible, inicializar directamente if (window.JitsiMeetExternalAPI) { initJitsi(); } return; } // Cargar Jitsi script desde el dominio configurado const script = document.createElement("script"); script.src = scriptSrc; script.async = true; script.onload = () => initJitsi(); script.onerror = () => { console.error(`Error al cargar el script de Jitsi desde ${scriptSrc}`); setAccessDenied(true); setDenialReason("No se pudo conectar con el servidor de videollamadas"); }; document.body.appendChild(script); return () => { if (jitsiApi.current) { jitsiApi.current.dispose(); jitsiApi.current = null; } isInitialized.current = false; // No eliminar el script aquí para evitar conflictos }; }, [status, session, jitsiDomain, jitsiRoomName, initJitsi]); if (status === "loading" || loading) { return (
); } if (!session) { redirect("/auth/login"); } // Mostrar pantalla de acceso denegado si no cumple las condiciones if (accessDenied) { return (
Acceso no permitido

{denialReason}

{appointment?.fechaSolicitada && (

Estado de la cita

{getAppointmentTimeStatus(appointment.fechaSolicitada)}

)}
); } const isDoctor = session.user.role === "DOCTOR"; const appointmentId = params.id as string; return ( <>
{/* Videollamada - 2 columnas en pantallas grandes */}
{appointment?.record && ( )}
{/* Notas de consulta - 1 columna en pantallas grandes */}
{/* Records Modal */} setShowRecordsModal(false)} onCopyContent={handleCopyContent} onDownloadReport={handleDownloadReport} onGeneratePDF={handleGeneratePDF} /> {/* Modal de confirmación de salida */}
¿Salir de la videollamada?
Si sales ahora, la videollamada se cerrará. {isDoctor && "Asegúrate de haber guardado las notas de consulta si las tienes."}
); }